package gl.drawer

import gl.checkAndThrowGLException
import gl.util.GLUtils
import gl.window
import org.joml.Matrix4f
import org.lwjgl.glfw.GLFW
import org.lwjgl.opengl.GL46C.*
import org.lwjgl.stb.STBImage

class SkyBox: Drawer() {

    // 顶点坐标
    private val vertices = floatArrayOf(
        -1f, -1f, -1f,
        -1f, -1f,  1f,
        -1f,  1f, -1f,
        -1f,  1f,  1f,
         1f, -1f, -1f,
         1f, -1f,  1f,
         1f,  1f, -1f,
         1f,  1f,  1f,
    )

    private val indices = intArrayOf(
        0, 1, 2, 3, 6, 7, 4, 5,
        2, 6, 0, 4, 1, 5, 3, 7,
    )

    private var progBox = 0
    private var progObj = 0
    private val vao = IntArray(2)

    private var locBTex = 0
    private var locBMvp = 0
    private var locBMv = 0
    private var locOTex = 0
    private var locOMvp = 0
    private var locOMv = 0

    private val textureBoxBuffers = IntArray(6)
    private val textureObjBuffers = IntArray(6)
    private var textureBox = 0
    private var textureObj = 0
    private val bufferRender = IntArray(2)

    private var mvp = Matrix4f().identity()

    override fun initialize() {
        // buffer vertex
        glGenBuffers(bufferRender)
        glBindBuffer(GL_ARRAY_BUFFER, bufferRender[0])
        glBufferStorage(GL_ARRAY_BUFFER, vertices, 0)
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bufferRender[1])
        glBufferStorage(GL_ELEMENT_ARRAY_BUFFER, indices, 0)
        // program texture
        progBox = newProgram(vBoxSrc, fBoxSrc)
        locBTex = glGetUniformLocation(progBox, "tex")
        locBMvp = glGetUniformLocation(progBox, "mvp")
        locBMv = glGetUniformLocation(progBox, "mv")
        progObj = newProgram(vObjSrc, fObjSrc)
        locOTex = glGetUniformLocation(progObj, "tex")
        locOMvp = glGetUniformLocation(progObj, "mvp")
        locOMv = glGetUniformLocation(progObj, "mv")
        glUseProgram(progBox)
        glGenVertexArrays(vao)
        glBindVertexArray(vao[0])
        glEnableVertexAttribArray(0)
        glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0)
        glBindVertexArray(vao[1])
        glEnableVertexAttribArray(0)
        glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0)
        checkAndThrowGLException()
        // textures
        textureBox = loadCubeImages(0, "square", textureBoxBuffers)
        textureObj = loadCubeImages(1, "box", textureObjBuffers)

        glEnable(GL_DEPTH_TEST)
        glDepthFunc(GL_LEQUAL)
        glDisable(GL_CULL_FACE)

        glBindBuffer(GL_ARRAY_BUFFER, bufferRender[0])
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bufferRender[1])

        onResize(window.width, window.height)
    }

    override fun update(): Boolean {
        super.update()
        val t = GLFW.glfwGetTime().toFloat() / 0x3FFF

        glUseProgram(progBox)
        val bMv = Matrix4f().identity()
        glUniformMatrix4fv(locBMvp, mvp)
        glUniformMatrix4fv(locBMv, bMv)
        glUniform1i(locBTex, 0)
        glBindVertexArray(vao[0])
        glDrawElements(GL_TRIANGLE_STRIP, 8, GL_UNSIGNED_INT, 0)
        glDrawElements(GL_TRIANGLE_STRIP, 8, GL_UNSIGNED_INT, 32)

        glUseProgram(progObj)
        val oMv = Matrix4f().identity()
            .translate(0f, 0f, -120f)
            .rotateY(80 * 3 * t)
            .rotateZ(70 * 3 * t)
        glUniformMatrix4fv(locOMvp, mvp)
        glUniformMatrix4fv(locOMv, oMv)
        glUniform1i(locOTex, 1)
        glBindVertexArray(vao[1])
        glDrawElements(GL_TRIANGLE_STRIP, 8, GL_UNSIGNED_INT, 0)
        glDrawElements(GL_TRIANGLE_STRIP, 8, GL_UNSIGNED_INT, 32)

        GLUtils.capture()
        return false
    }

    override fun terminal() {
        glBindVertexArray(0)
        glDeleteVertexArrays(vao)
        glBindTexture(GL_TEXTURE_2D, 0)
        glBindTexture(GL_TEXTURE_CUBE_MAP, 0)
        glDeleteTextures(textureBox)
        glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0)
        glDeleteBuffers(textureBoxBuffers)
        glBindBuffer(GL_ARRAY_BUFFER, 0)
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)
        glDeleteBuffers(bufferRender)
        glUseProgram(0)
        glDeleteProgram(progObj)
        glDeleteProgram(progBox)
    }

    override fun onResize(width: Int, height: Int) {
        super.onResize(width, height)
        mvp = Matrix4f()
            .identity()
            .perspective(35f, width.toFloat() / height, 0.1f, 500f)
            .scale(1f, -1f, 1f)
    }

    private fun newProgram(v: String, f: String): Int {
        val program = glCreateProgram()
        val vs = glCreateShader(GL_VERTEX_SHADER)
        glShaderSource(vs, v)
        glCompileShader(vs)
        val fs = glCreateShader(GL_FRAGMENT_SHADER)
        glShaderSource(fs, f)
        glCompileShader(fs)
        glAttachShader(program, vs)
        glAttachShader(program, fs)
        glLinkProgram(program)
        glDetachShader(program, vs)
        glDetachShader(program, fs)
        glDeleteShader(vs)
        glDeleteShader(fs)
        return program
    }

    private fun loadCubeImages(unit: Int, folder: String, buffers: IntArray): Int {
        val image = glGenTextures()
        glGenBuffers(buffers)
        val box = loadImageToBuffer("$folder\\left.png", buffers[0])
        loadImageToBuffer("$folder\\right.png", buffers[1])
        loadImageToBuffer("$folder\\top.png", buffers[2])
        loadImageToBuffer("$folder\\bottom.png", buffers[3])
        loadImageToBuffer("$folder\\back.png", buffers[4])
        loadImageToBuffer("$folder\\front.png", buffers[5])
        checkAndThrowGLException()
        val skySize = box[0]
        glActiveTexture(GL_TEXTURE0 + unit)
        glBindTexture(GL_TEXTURE_CUBE_MAP, image)
        glTexStorage2D(GL_TEXTURE_CUBE_MAP, 1, GL_RGBA8, skySize, skySize)
        glBindBuffer(GL_PIXEL_UNPACK_BUFFER, buffers[0])
        glTexSubImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, 0, 0, skySize, skySize, GL_RGBA, GL_UNSIGNED_BYTE, 0)
        glBindBuffer(GL_PIXEL_UNPACK_BUFFER, buffers[1])
        glTexSubImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, 0, 0, skySize, skySize, GL_RGBA, GL_UNSIGNED_BYTE, 0)
        glBindBuffer(GL_PIXEL_UNPACK_BUFFER, buffers[2])
        glTexSubImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Y, 0, 0, 0, skySize, skySize, GL_RGBA, GL_UNSIGNED_BYTE, 0)
        glBindBuffer(GL_PIXEL_UNPACK_BUFFER, buffers[3])
        glTexSubImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, 0, 0, skySize, skySize, GL_RGBA, GL_UNSIGNED_BYTE, 0)
        glBindBuffer(GL_PIXEL_UNPACK_BUFFER, buffers[4])
        glTexSubImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Z, 0, 0, 0, skySize, skySize, GL_RGBA, GL_UNSIGNED_BYTE, 0)
        glBindBuffer(GL_PIXEL_UNPACK_BUFFER, buffers[5])
        glTexSubImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, 0, 0, skySize, skySize, GL_RGBA, GL_UNSIGNED_BYTE, 0)
        return image
    }

    private fun loadImageToBuffer(image: String, buffer: Int): IntArray {
        val path = "D:\\lq\\gl\\app\\src\\main\\resources\\$image"
        glBindBuffer(GL_PIXEL_UNPACK_BUFFER, buffer)
        val width = IntArray(1)
        val height = IntArray(1)
        val channel = IntArray(1)
        val imageBuf = STBImage.stbi_load(path, width, height, channel, 4)!!
        glBufferStorage(GL_PIXEL_UNPACK_BUFFER, imageBuf, 0)
        STBImage.stbi_image_free(imageBuf)
        return intArrayOf(width[0], height[0], channel[0])
    }

    private fun glUniformMatrix4fv(location: Int, matrix: Matrix4f) {
        val values = FloatArray(16)
        matrix[values]
        glUniformMatrix4fv(location, false, values)
    }
}

private val vBoxSrc = """
#version 400 core

uniform mat4 mv;
uniform mat4 mvp;

layout(location=0) in vec3 vPosition;
out vec3 texCoord;

void main() {
    texCoord = vPosition;
    gl_Position = mv * mvp * vec4(vPosition, 1.0);
}
""".trimIndent()

private val fBoxSrc = """
#version 400 core

uniform samplerCube tex;

in vec3 texCoord;

out vec4 fColor;

void main() {
    fColor = texture(tex, texCoord);
}
""".trimIndent()

private val vObjSrc = """
#version 400 core

layout(location = 0) in vec4 vPosition;
layout(location = 1) in vec3 vNormal;

out vec3 normal;
out vec3 position;

uniform mat4 mvp;
uniform mat4 mv;

void main() {
    gl_Position = mv * mvp * vPosition;
    normal = mat3(mv) * vNormal;
    position = (mv * vPosition).xyz;
}
""".trimIndent()

private val fObjSrc = """
#version 400 core

in vec3 normal;
in vec3 position;

out vec4 fColor;

uniform samplerCube tex;

void main() {
    vec3 tc = reflect(position, normalize(normal));
    fColor = vec4(0.3, 0.2, 0.1, 1.0) +
             vec4(0.97, 0.83, 0.79, 0.0) * texture(tex, tc);
}
""".trimIndent()